Composing a box plot chart
What is box plot?
A box plot is a standardized way of displaying the distribution of data based on the five number summary: minimum, first quartile, median, third quartile, and maximum. It simply represents statistical data on a plot in which a rectangle is drawn to represent the second and third quartiles, usually with a vertical line inside to indicate the median value. The lower and upper quartiles are shown as horizontal lines either side of the rectangle.
Layers definition
Muze does not have a high level mark (plot type) called BoxPlot. However, Muze exposes API using which you can compose layers which represent highlevel mark like BoxPlot using low level marks like bar, line, etc.
Steps to create box plot
Following are the steps we need to do to create the above visualization:
- Create an instance of DataModel from data and schema
- Provide rows and columns to the canvas for axes
- Provide color encoding to the canvas
- Add layers to the canvas for composing layers in the visualization
- Add title and subtitle to canvas
- Finally, mount the chart to the DOM
Create an instance of DataModel from data and schema
The execution environment has stored the data and schema in data and schema variable. DataModel is retrieved from muze namespace. Here we write the following code to create an instance of DataModel from JSON data:
const { muze } = viz;
const data = [
{
organ: "petalWidth",
minValue: 0.1,
maxValue: 2.5,
meanValue: 1.199333333333334,
quarter: 0.6496666666666671,
thirdQuarter: 1.849666666666667,
},
{
organ: "petalLength",
minValue: 1,
maxValue: 6.9,
meanValue: 3.7580000000000027,
quarter: 2.3790000000000013,
thirdQuarter: 5.3290000000000015,
},
{
organ: "sepalWidth",
minValue: 2,
maxValue: 4.4,
meanValue: 3.057333333333334,
quarter: 2.528666666666667,
thirdQuarter: 3.7286666666666672,
},
{
organ: "sepalLength",
minValue: 4.3,
maxValue: 7.9,
meanValue: 5.843333333333335,
quarter: 5.071666666666667,
thirdQuarter: 6.871666666666668,
},
];
const schema = [
{
name: "organ",
type: "dimension",
},
{
name: "minValue",
type: "measure",
},
{
name: "meanValue",
type: "measure",
},
{
name: "maxValue",
type: "measure",
},
{
name: "quarter",
type: "measure",
},
{
name: "thirdQuarter",
type: "measure",
},
];
const formattedData = await DataModel.loadData(data, schema);
let dm = new DataModel(formattedData);
Assign fields to encoding channel
Based on what we what we want to see in X and Y-axis, we have to call rows and columns with the correct fields to achieve the above chart.
If you look at the layer definition file carefully, the boxMark expects multiple variable to draw the mark completely. Hence, Our Y-axis is created from multiple variables (fields) of same unit.
Let's deviate from this example to see why a shared variable is needed to plot multiple variables in one axis. We assume, we have age field which records age of every user who use a product. We want to see max age and min age of users over the years as a range plot like the following chart.
Now both fields (max age and min age) is derived from age, hence min and max age is of type age only. There is no harm to put those two fields in same axis.
But we can't do it in Muze by passing those two fields in rows and columns as planer encoding methods do not support creating single instance of axis from two different fields until explicitly mentioned. share operator takes multiple similar fields and create a SharedField which can be used to create an axis.
Coming to our BoxPlot example, the Y-Axis is created using a SharedField derived by applying share operator on minValue, meanValue, maxValue, quarter, thirdQuarter. organ is plotted on X-Axis.
let columns = [
(sharedField = share(
"minValue",
"meanValue",
"maxValue",
"quarter",
"thirdQuarter",
)),
];
let rows = ["organ"];
canvas.rows(columns).columns(rows).color("organ");
All we have done is create a Y-Axis from multiple fields and pass those individual fields to boxMark.
Add layers to visualization
The visuals of BoxPlot is achieved by composing atomic layers which Muze provides out of the box.
Once the definition is created, we can register the definition in Muze's layer registry and use it as simple marks.
let layerFactory = muze.layerFactory;
// The layers we need are
var layers = [
{
name: "maxTick",
mark: "tick",
className: "boxTicks",
encoding: {
y: "boxMark.encoding.maxValue",
x: "boxMark.encoding.x",
},
interactive: false,
},
{
name: "upperTick",
className: "upper-tick",
mark: "tick",
encoding: {
y: "boxMark.encoding.quarter",
x: "boxMark.encoding.x",
y0: "boxMark.encoding.minValue",
},
interactive: false,
},
{
name: "upperBand",
mark: "bar",
className: "upperBand",
encoding: {
y: "boxMark.encoding.thirdQuarter",
x: "boxMark.encoding.x",
y0: "boxMark.encoding.meanValue",
color: "boxMark.encoding.color",
},
transform: {
type: "identity",
},
},
{
name: "meanTick",
mark: "tick",
className: "boxTicks",
encoding: {
y: "boxMark.encoding.meanValue",
x: "boxMark.encoding.x",
},
interactive: false,
},
{
name: "lowerBand",
mark: "bar",
className: "lowerBand",
encoding: {
y0: "boxMark.encoding.meanValue",
x: "boxMark.encoding.x",
y: "boxMark.encoding.quarter",
color: "boxMark.encoding.color",
},
transform: {
type: "identity",
},
},
{
name: "lowerTick",
mark: "tick",
className: "boxTicks",
encoding: {
y: "boxMark.encoding.maxValue",
x: "boxMark.encoding.x",
y0: "boxMark.encoding.thirdQuarter",
},
interactive: false,
},
{
name: "minTick",
mark: "tick",
className: "boxTicks",
encoding: {
y: "boxMark.encoding.minValue",
x: "boxMark.encoding.x",
},
interactive: false,
},
];
// Compose all layers into one
layerFactory.composeLayers("boxMark", layers);
We use the custom layer in our code as follows:
.layers([{
mark: 'boxMark',
encoding: { // Map the encoding with variables. These custom encodings are used in the composite layers.
minValue: 'minValue',
meanValue: 'meanValue',
x: 'organ',
maxValue: 'maxValue',
quarter: 'quarter',
thirdQuarter: 'thirdQuarter'
}
}])
Add title and subtitle to canvas
Add a title and subtitle to the canvas:
canvas
.title("Composing Boxplot", { position: "bottom", align: "right" })
.subtitle("Iris sepalLength distrubution", {
position: "bottom",
align: "right",
});
Mount the chart to the DOM
Finally attach the canvas instance to DOM which houses the visualization:
canvas.mount("#chart");
An element with id chart is available in the DOM to house the visualization.
Example
const { muze } = viz;
const data = [
{
organ: "petalWidth",
minValue: 0.1,
maxValue: 2.5,
meanValue: 1.199333333333334,
quarter: 0.6496666666666671,
thirdQuarter: 1.849666666666667,
},
{
organ: "petalLength",
minValue: 1,
maxValue: 6.9,
meanValue: 3.7580000000000027,
quarter: 2.3790000000000013,
thirdQuarter: 5.3290000000000015,
},
{
organ: "sepalWidth",
minValue: 2,
maxValue: 4.4,
meanValue: 3.057333333333334,
quarter: 2.528666666666667,
thirdQuarter: 3.7286666666666672,
},
{
organ: "sepalLength",
minValue: 4.3,
maxValue: 7.9,
meanValue: 5.843333333333335,
quarter: 5.071666666666667,
thirdQuarter: 6.871666666666668,
},
];
const schema = [
{
name: "organ",
type: "dimension",
},
{
name: "minValue",
type: "measure",
},
{
name: "meanValue",
type: "measure",
},
{
name: "maxValue",
type: "measure",
},
{
name: "quarter",
type: "measure",
},
{
name: "thirdQuarter",
type: "measure",
},
];
const layers = [
{
name: "maxTick",
mark: "tick",
className: "boxTicks",
encoding: {
y: "boxMark.encoding.maxValue",
x: "boxMark.encoding.x",
size: {
value: () => 0.001,
},
},
interactive: false,
},
{
name: "upperTick",
className: "upper-tick",
mark: "tick",
encoding: {
y: "boxMark.encoding.quarter",
x: "boxMark.encoding.x",
y0: "boxMark.encoding.minValue",
size: {
value: () => 0.001,
},
},
interactive: false,
},
{
name: "upperBand",
mark: "bar",
className: "upperBand",
encoding: {
y: "boxMark.encoding.thirdQuarter",
x: "boxMark.encoding.x",
y0: "boxMark.encoding.meanValue",
color: "boxMark.encoding.color",
},
transform: {
type: "identity",
},
},
{
name: "meanTick",
mark: "tick",
className: "boxTicks",
encoding: {
y: "boxMark.encoding.meanValue",
x: "boxMark.encoding.x",
size: {
value: () => 0.001,
},
},
interactive: false,
},
{
name: "lowerBand",
mark: "bar",
className: "lowerBand",
encoding: {
y0: "boxMark.encoding.meanValue",
x: "boxMark.encoding.x",
y: "boxMark.encoding.quarter",
color: "boxMark.encoding.color",
},
transform: {
type: "identity",
},
},
{
name: "lowerTick",
mark: "tick",
className: "boxTicks",
encoding: {
y: "boxMark.encoding.maxValue",
x: "boxMark.encoding.x",
y0: "boxMark.encoding.thirdQuarter",
size: {
value: () => 0.001,
},
},
interactive: false,
},
{
name: "minTick",
mark: "tick",
className: "boxTicks",
encoding: {
y: "boxMark.encoding.minValue",
x: "boxMark.encoding.x",
size: {
value: () => 0.001,
},
},
interactive: false,
},
];
const formattedData = await DataModel.loadData(data, schema);
let dm = new DataModel(formattedData);
// Registry for user defined layers
var layerFactory = muze.layerFactory;
// Compose share operator for plotting multiple variable in one Y-axis
var share = muze.Operators.share;
var canvas = env.canvas();
// Use the custom layer definition to register a new layer and name it boxMark
layerFactory.composeLayers("boxMark", layers);
muze
.canvas()
.rows([share("minValue", "meanValue", "maxValue", "quarter", "thirdQuarter")])
.columns(["organ"])
.color("organ")
.layers([
{
mark: "boxMark",
encoding: {
// Map the encoding with variables. These custom encodings are used in the composite layers.
minValue: "minValue",
meanValue: "meanValue",
x: "organ",
maxValue: "maxValue",
quarter: "quarter",
thirdQuarter: "thirdQuarter",
},
},
])
.config({
border: {
showValueBorders: {
right: false,
bottom: false,
},
},
axes: {
y: {
showAxisName: true,
name: "Measure",
alignZeroLine: true,
},
},
})
.data(dm)
.width(700)
.height(700)
.mount("#chart");